<?php

/**
 * Контроллер и контейнер компонентов многостраничной формы.
 *
 */
abstract class Kernel_Form_Wizard extends Kernel_Controller_Tokened implements Kernel_Form_Storage 
{
    
    /**
     * Наименование переменной сессии, в которой хранятся данные формы.
     *
     */
    const VARNAME = 'wizard';

    /**
     * Наименования шагов формы в той последовательности, в которой они должны
     * выполняться и сквозные параметры, которые должны передаваться в эти шаги.
     *
     * @var array
     */
    protected $_steps = array();

    /**
     * Описание ключа записей, редактируемых в форме.
     *
     * @var array
     */
    protected $_key = array();

    /**
     * Сохраняет ли фильтр параметры для вложенных фильтров.
     *
     * @var boolean
     */
    protected $_through = true;

    /**
     * Путь к файлу шаблона страницы с сообщением о том, что форма сохранена.
     *
     * @var string
     */
    protected $_template_saved = 'blank.tpl'; 

    /**
     * Наименование текущего шага формы.
     *
     * @var string
     */
    protected $_step;

    /**
     * Идентификатор текущей сессии многостраничной формы.
     *
     * @var string
     */
    protected $_session; 
    
    /**
     * Флаг, указывающий, что после сохранения данных формы не нужно показывать
     * сответствующее сообщение.
     *
     * @var boolean
     */
    protected $_no_message = false;
    
    /**
     * Конструктор.
     *
     */
    public function __construct($params = array()) 
    {
        parent::__construct($params);
        
        if ($this->_sessionValid($session = Application::getRequest()->get('session'))) {
            $this->_session = $session;
        } else {
            $this->_sessionStart();
        }
        
        // get name of the current step
        $this->_step = Application::getRequest()->get('step');
    }

    /**
     * Загружает данные формы.
     *
     * @return  array
     */
    public function load() 
    {
        return $this->_loadStep($this->_step);
    }

    /**
     * Возвращает сохраненные на указанном шаге данные формы.
     *
     * @param   string  $step
     * @return  array
     */
    public function get($step = null) 
    {
        return null !== $step
            ? ($this->_passed($step) ? $this->_get($step) : array())
            : $_SESSION[self::VARNAME][$this->_session]['data'];
    }

    /**
     * Сохраняет данные формы. Также можно указать шаг вручную.
     * 
     * 
     * @param   array   $values
     * @param   string  $step
     */
    public function save($values, $step = null) 
    {
        $step = !is_null($step) ? $step : $this->_step;
        $this->_saveStep($step, $values);
    }

    /**
     * @desc Call after form data is saved. 
     *
     */
    public function done() 
    {
        if (false !== ($next = $this->_next())) {
            // go to the next step
            $this->_redirect(array('step' => $next));
        } else {
            // on the last step save multipage form
            $this->_save();
            $this->_end();
        }
    }
    
    /**
    * @desc In completion ...
    * 
    */
    protected function _end() 
    {
        if (!$this->_no_message) {
            Application::getResponse()->message(true, array(get_class($this), 'ADDED'));
        }
        
        $this->_clear();
        
        Application::getResponse()->setTemplate($this->_template_saved);
    }

    /**
     * Возвращает описание компонента, который обработает действие, полученное
     * из запроса.
     *
     * @return  mixed
     */
    protected function _getFilterToken() 
    {
        if (!is_string($this->_step)) {
            // выполняем внешнее перенаправление на шаг по умолчанию
            $this->_redirect(array(), true);
        }
        
        if (!array_key_exists($this->_step, $this->_steps)) {
            $this->_abort('Step not found');
        }
        
        $allowed = $this->_isAllowed($this->_step);
        
        switch (true) {
            case false === $allowed:
                $this->_deny('No one step is allowed');
                break;
            case is_string($allowed):
                $this->_redirect(array('step' => $allowed));
                break;
        }
        return $this->_token($this->_step);
    }

    /**
     * Возвращает наименование класса компонента, который обработает запрошенное
     * действие.
     *
     * @param   string  $name
     * @return  string
     */
    protected function _getFilterClassName($name) 
    {
        return get_class($this) . '_' . str_camelize($name);
    }

    /**
     * @desc Return component params of the specified step. 
     *
     * @param   string  $step
     * @return  array
     */
    protected function _params($step) 
    {
        return array_merge(
            array_intersect_key($this->_params, array_flip($this->_steps[$step])),
            array('storage' => $this)
        );
    }

    /**
     * @desc Load form data on the specified step
     *
     * @param   string  $step
     * @return  array
     */
    protected function _loadStep($step) 
    {
        return $this->_passed($step) ? $this->_get($step) : $this->_fetch($step);
    }

    /**
     * @desc Save form data on the specified step
     *
     * @param   string  $step
     * @param   array   $values
     */
    protected function _saveStep($step, $values) 
    {
        $_SESSION[self::VARNAME][$this->_session]['data'][$step] = 
            array_merge(
                @(array)$_SESSION[self::VARNAME][$this->_session]['data'][$step], 
                $values
            );
    }

    /**
     * @desc Get initial data for the form on the specified step. 
     *
     * @param   string  $step
     * @return  array
     */
    protected function _fetch($step) 
    {
        // default inintialize boot data from the form itself
        return $this->_isNew() ? array() : $this->_step($step)->load();
    }

    /**
     * @desc Save data of the multipage form. 
     *
     */
    protected function _save() 
    {
        // default, simply save every page (form) data
        foreach (array_keys($this->_steps) as $step) {
            $this->_step($step)->save($this->get($step));
        }
    }

    /**
     * @desc Clear session data. 
     *
     */
    protected function _clear() 
    {
        unset($_SESSION[self::VARNAME][$this->_session]);
    }

    /**
     * @desc Get form value for the specified step. 
     *
     * @param   string  $step
     * @param   string  $name
     * @return  mixed
     */
    protected function _getValue($step, $name = null) 
    {
        if ($this->_passed($step)) {
            $vars = $this->_get($step);
            return null === $name
                ? $vars
                : get_array_element($vars, $name);
        }
        return null;
    }

    /**
     * @desc Return whether the specified step was passed successfully. 
     *
     * @param   string  $step
     * @return  boolean
     */
    protected function _passed($step) 
    {
        return isset($_SESSION[self::VARNAME][$this->_session]['data'][$step]);
    }

    /**
     * @desc Return name of the previous step.
     *
     * @return  string
     */
    protected function _previous() 
    {
        return $this->_offset(-1);
    }

    /**
     * @desc Return name of the next step. 
     *
     * @return  string
     */
    protected function _next() 
    {
        return $this->_offset(+1);
    }
    
    /**
     * @desc Return unique key of the editing step in original or string form.
     *
     * @param   boolean $serialized
     * @return  array
     */
    private function _key($serialized = false) 
    {
        $key = array_extract($this->_params, $this->_key, null);
        return $serialized ? md5(serialize($key)) : $key;
    }

    /**
     * @desc Return whether form is a new one. 
     *
     * @return  boolean
     */
    private function _isNew() 
    {
        return !array_filter($this->_key());
    }

    /**
     * @desc Return token refers to the current step.
     *
     * @param   string  $step
     * @return  Kernel_Token
     */
    private function _token($step) 
    {
        return new Kernel_Token($step, $this->_params($step));
    }

    /**
     * @desc Get form refers to the current step. 
     *
     * @param   string  $step
     * @return  Kernel_Action_Form
     */
    private function _step($step) 
    {
        return $this->_construct($this->_token($step));
    }

    /**
     * @desc Return whether user is allowed to be on the current step. Default, 
     * browse all the components (steps) till the current one inclusive. 
     *
     * @param   string  $name
     * @return  boolean
     */
    private function _isAllowed($name) 
    {
        foreach (array_keys($this->_steps) as $step) {
            if ($name == $step) {
                return true;
            }
            if (!$this->_passed($step)) {
                return $step;
            }
        }
        return false;
    }

    /**
     * @desc Redirect to the specified step/session. 
     *
     * @param   array   $params
     * @param   boolean $permanent
     */
    protected function _redirect($params, $permanent = false) 
    {
        $this->_location($params + array(
            'session' => $this->_session,
            'step' => key($this->_steps),
        ), $permanent);
    }

    /**
     * @desc Return name of the next step that is offset to the current one.
     *
     * @return  string
     */
    private function _offset($offset) 
    {
        $steps = array_keys($this->_steps);
        return false !== ($current = array_search($this->_step, $steps))
            && array_key_exists($key = $current + $offset, $steps) ? $steps[$key] : false;
    }

    /**
     * @desc Get data of the specified step. 
     *
     * @param   string  $step
     * @return  array
     */
    private function _get($step) 
    {
        return $_SESSION[self::VARNAME][$this->_session]['data'][$step];
    }

    /**
     * @desc Check whether specified SESSION ID is valid.
     *
     * @param   string  $session
     * @return  boolean
     */
    private function _sessionValid($session) 
    {
        return isset($_SESSION[self::VARNAME][$session]['key'])
            && $this->_key() == $_SESSION[self::VARNAME][$session]['key'];
    }

    /**
     * @desc Create SESSION ID. 
     *
     * @return  string
     */
    private function _sessionStart() 
    {
        // generate session id
        $session = md5(uniqid(rand()));
        
        // fill session data with initial values
        $_SESSION[self::VARNAME][$session] = array(
            'key' => $this->_key(),
            'data' => array(),
        );
        
        // redirect client (obviously indicate session id)
        $this->_redirect(array('session' => $session));
    }
    
}
